package com.hzw.saas.web.app.workspace.controller;

import java.io.IOException;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;

import com.hzw.saas.api.storage.ICatalogService;
import com.hzw.saas.api.storage.IStorageService;
import com.hzw.saas.api.storage.bo.InxChunkBO;
import com.hzw.saas.api.storage.bo.InxChunkDataBO;
import com.hzw.saas.api.storage.enums.StorageSpaceEnum;
import com.hzw.saas.api.workspace.IWorkspaceResourceService;
import com.hzw.saas.api.workspace.bo.UserWorkspaceFileBo;
import com.hzw.saas.common.config.annotation.SysLog;
import com.hzw.saas.common.config.exception.DefaultSaasException;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.security.utils.SecurityUtils;
import com.hzw.saas.common.storage.pojo.FileStorage;
import com.hzw.saas.common.storage.pojo.InxChunkMetadata;
import com.hzw.saas.common.storage.pojo.InxChunkStorage;
import com.hzw.saas.common.storage.service.IChunkStorageService;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.PathUtils;
import com.hzw.saas.common.util.SnowFlakeUtil;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import com.hzw.saas.common.util.exception.ForbiddenException;
import com.hzw.saas.service.storage.config.StorageConfig;
import com.hzw.saas.web.app.workspace.dto.ResourceFilePostDTO;
import com.hzw.saas.web.app.workspace.param.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.extra.servlet.ServletUtil;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.*;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * <p>
 *
 * </p>
 *
 * @author sonam
 * @since 2021/2/2 9:51
 */
@Api(tags = "云控制台/云桌面空间")
@Slf4j
@RequestMapping("/workspace/")
@RestController
@RequiredArgsConstructor
public class WorkspaceResourceController {

    private final StorageConfig storageConfig;
    private final ICatalogService catalogService;
    private final IStorageService storageService;

    private final IWorkspaceResourceService workspaceResourceService;
    private final IChunkStorageService chunkStorageService;

    private String chunkSupPath;

    @PostConstruct
    public void init() {
        chunkSupPath = storageConfig.getWorkSpaceCacheCatalog();
    }

    @GetMapping("/resource/list")
    @SysLog
    @ApiOperation(value = "获取文件列表", notes = "获取文件列表")
    @ApiOperationSort(1)
    public ResponseEntity<List<UserWorkspaceFileBo>> getFileList(@Validated WPResourceGetParam wpResourceGetParam) {
        checkPath(wpResourceGetParam.getPath(), true);
        return ResponseEntity.ok(workspaceResourceService.getFileList(SecurityUtils.getUser().getUserId(),
            wpResourceGetParam.getPath()));
    }

    @PostMapping("/resource/dir")
    @SysLog(operation = "创建目录", mode = 3)
    @ApiOperation(value = "创建目录", notes = "创建文件夹")
    @ApiOperationSort(2)
    @ApiResponse(code = 200, message = "创建目录成功")
    public ResponseEntity<Void> createDir(@Validated @RequestBody WPResourceCreateParam wpResourceCreateParam) {
        String dirName = wpResourceCreateParam.getDirName();
        checkPath(wpResourceCreateParam.getParentPath(), true);
        workspaceResourceService.createDir(SecurityUtils.getUser().getUserId(),
            PathUtils.connectPath(wpResourceCreateParam.getParentPath(), dirName));
        return ResponseEntity.ok().build();
    }

    @DeleteMapping("/resource/file")
    @SysLog(operation = "删除目录或者文件", mode = 3)
    @ApiOperation(value = "删除目录或者文件", notes = "删除目录或者文件")
    @ApiResponse(code = 200, message = "删除目录或者文件成功")
    @ApiOperationSort(3)
    public ResponseEntity<Void> remove(@Validated @RequestBody WPResourceDelParam wpResourceDelParam) {
        checkPath(wpResourceDelParam.getPath(), false);
        workspaceResourceService.remove(SecurityUtils.getUser().getUserId(), wpResourceDelParam.getPath(),
            wpResourceDelParam.getForceRemove());
        return ResponseEntity.ok().build();
    }

    @PutMapping("/resource/dir")
    @SysLog(operation = "重命名目录或者文件", mode = 3)
    @ApiOperation(value = "重命名目录或者文件", notes = "重命名文件夹（先复制再删除）")
    @ApiResponse(code = 200, message = "重命名目录成功")
    @ApiOperationSort(4)
    public ResponseEntity<Void> rename(@Validated @RequestBody WPResourceRenameParam wpResourceRenameParam) {
        checkPath(wpResourceRenameParam.getPath(), true);
        workspaceResourceService.rename(SecurityUtils.getUser().getUserId(),
            wpResourceRenameParam.getPath(),
            wpResourceRenameParam.getNewName());
        return ResponseEntity.ok().build();
    }


    @PostMapping("/resource/move/dir")
    @SysLog(operation = "移动文件或者目录", mode = 3)
    @ApiOperation(value = "移动文件或者目录", notes = "移动文件或者目录")
    @ApiResponse(code = 200, message = "移动成功")
    @ApiOperationSort(5)
    public ResponseEntity<Void> move(@Validated @RequestBody WPResourceMvParam wpResourceMvParam) {
        checkPath(wpResourceMvParam.getTargetPath(), true);
        checkPath(wpResourceMvParam.getPath(), true);
        workspaceResourceService.move(SecurityUtils.getUser().getUserId(),
            wpResourceMvParam.getPath(),
            wpResourceMvParam.getTargetPath());
        return ResponseEntity.ok().build();
    }

    @PostMapping("/resource/copy/dir")
    @SysLog(operation = "复制文件或者目录", mode = 3)
    @ApiOperation(value = "复制文件或者目录", notes = "复制文件或者目录")
    @ApiResponse(code = 200, message = "复制成功")
    @ApiOperationSort(6)
    public ResponseEntity<Void> copy(@Validated @RequestBody WPResourceCopyParam wpResourceCopyParam) {
        checkPath(wpResourceCopyParam.getTargetPath(), true);
        checkPath(wpResourceCopyParam.getPath(), true);
        workspaceResourceService.copy(SecurityUtils.getUser().getUserId(),
            wpResourceCopyParam.getPath(),
            wpResourceCopyParam.getTargetPath());
        return ResponseEntity.ok().build();
    }

    // zzl@0525 工作空间与个人空间统一计算容量
    // @GetMapping("/resource/totalSize/")
    // @ApiOperation(value = "查看工作空间容量查看", notes = "查询工作空间的已用容量以及总容量")
    // @ApiOperationSort(7)
    // public ResponseEntity<SpaceSizeDto> getSpaceSize() {
    //     String userId = SecurityUtils.getUser().getUserId();
    //     long size = workspaceResourceService.getFileSize(userId, "/");
    //     SpaceSizeDto spaceSizeDto = new SpaceSizeDto();
    //     spaceSizeDto.setSize(1024l * 1024l);
    //     spaceSizeDto.setUsedSize(size);
    //     return ResponseEntity.ok(spaceSizeDto);
    // }

    @PostMapping("/resource/file/info")
    @SysLog(operation = "提交文件信息", mode = 3)
    @ApiOperation(value = "提交文件信息", notes = "提交资源文件信息，用于分片上传资源前获取服务器返回的资源唯一标识符(fileId)")
    @ApiResponses({
        @ApiResponse(code = 200, message = "返回资源标识ID,用于后续上传操作"),
        @ApiResponse(code = 409, message = "文件已存在"),
    })
    @ApiOperationSort(32)
    public ResponseEntity<ResourceFilePostDTO> saveFileInfo(@Validated @RequestBody ResourceFilePostParam param) {
        log.debug("post resource info request...");
        String userId = SecurityUtils.getUserId();
        // 判断文件大小是否超出
        this.assertNotOverLimitSize(param.getLength());
        // 判断用户存储空间是否足够
        storageService.assertNotOverSize(userId, StorageSpaceEnum.CLOUD_DESKTOP_SPACE.getCode(), param.getLength());
        String fileId = SnowFlakeUtil.nextIdStr();
        String fileSupPath = workspaceResourceService.dealPath(userId, param.getSupId());
        this.writeMetadata(fileId, fileSupPath, param.getName(), param.getLength());
        ResourceFilePostDTO result = new ResourceFilePostDTO(fileId);
        return ResponseEntity.ok(result);
    }

    private void writeMetadata(String fileId, String fileSupPath, String fileName, Long fileLength) {
        String chunkSupPath = catalogService.getCacheSupPath(this.chunkSupPath, fileId);
        InxChunkMetadata metadata = new InxChunkMetadata();
        metadata.setSuperPath(fileSupPath);
        metadata.setFileName(fileName);
        metadata.setLength(fileLength);
        chunkStorageService.writeMetadata(chunkSupPath, metadata);
    }

    /**
     * 单独校验每个分片
     *
     * @param chunkCheck
     * @return
     */
    @GetMapping("/resource/file/chunk")
    @SysLog
    @ApiOperation(value = "校验文件分片", notes = "用于分片插件开启分片上传校验时使用，如vue-simple-uploader中开启testChunks=true")
    @ApiResponses({
        @ApiResponse(code = 200, message = "分片已上传过"),
        @ApiResponse(code = 304, message = "分片未上传过"),
        @ApiResponse(code = 400, message = "请求参数异常"),
        @ApiResponse(code = 500, message = "服务器异常")
    })
    @ApiOperationSort(33)
    public ResponseEntity<Void> checkChunk(@Validated InxChunkBO chunkCheck) {
        log.debug("chunk check request...");
        // 设置分片校验状态信息
        // 默认返回非(200, 201, 202)状态码，正常走标准上传(不进行分片校验)
        int status = HttpServletResponse.SC_NOT_MODIFIED;
        // 检查上传状态
        String chunkSupPath = catalogService.getCacheSupPath(this.chunkSupPath, chunkCheck.getFileId());
        boolean exist = chunkStorageService.exist(chunkSupPath, String.valueOf(chunkCheck.getChunkNumber()));
        // 文件上传完成，可跳过上传（秒传）
        if (exist) {
            status = HttpServletResponse.SC_OK;
        }
        return ResponseEntity.status(status).build();
    }

    @PostMapping(value = "/resource/file/chunk", consumes = {"multipart/form-data"})
    @SysLog
    @ApiOperation(value = "上传文件分片", notes = "上传文件分片，该接口支持同一分片重复调用上传。上传文件前请先调用<提交个人空间文件信息>接口，获取服务器提供的文件ID")
    @ApiResponses({
        @ApiResponse(code = 200, message = "上传成功"),
        @ApiResponse(code = 400, message = "请求参数或请求类型异常"),
        @ApiResponse(code = 415, message = "文件类型不支持"),
        @ApiResponse(code = 500, message = "服务器内部错误")
    })
    @ApiOperationSort(35)
    public ResponseEntity<Boolean> uploadChunk(@Validated InxChunkDataBO param, HttpServletRequest request) {
        log.debug("chunk upload request...");
        // 判断请求类型
        boolean multipart = ServletUtil.isMultipart(request);
        if (!multipart) {
            throw DefaultSaasException.build("上传失败，请求参数异常，请使用multipart/form-data类型上传").status(HttpStatus.BAD_REQUEST);
        }
        try {
            // 此处直接将spring获取到的缓存文件放进内存
            // 这一步是为了防止后续在异步处理分片文件保存时，springboot将缓存文件删除
            byte[] data = param.getChunkFile().getBytes();
            // 分片保存
            InxChunkStorage chunkStorage = MapperUtil.nf().map(param, InxChunkStorage.class);
            chunkStorage.setSuperPath(catalogService.getCacheSupPath(this.chunkSupPath, param.getFileId()));
            chunkStorageService.saveChunk(chunkStorage, data);
            return ResponseEntity.ok(true);
        } catch (IOException e) {
            // 上传失败，更新资源状态
            log.error("chunk upload error", e);
            throw DefaultSaasException.build(e.getMessage());
        }
    }

    @PutMapping("/resource/file/{fileId}/chunks")
    @SysLog(operation = "完成文件上传", mode = 3)
    @ApiOperation(value = "完成文件上传", notes = "文件分片上传完成后执行该回调方法")
    @ApiImplicitParam(name = "fileId", value = "资源文件ID", required = true, dataType = "String")
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 400, message = "文件未上传完整"),
        @ApiResponse(code = 500, message = "服务器内部错误"),
    })
    @ApiOperationSort(36)
    public ResponseEntity<Boolean> mergeChunks(@NotBlank(message = "资源ID不可为空") @PathVariable String fileId) {
        log.debug("mergeChunks request...");
        // 合并分片
        FileStorage fileStorage = new FileStorage();
        String chunkSupPath = catalogService.getCacheSupPath(this.chunkSupPath, fileId);
        chunkStorageService.mergeChunks(fileStorage, chunkSupPath, true, ConflictModeEnum.RENAME_SRC);
        return ResponseEntity.ok(true);
    }

    @DeleteMapping("/resource/file/{fileId}/chunks")
    @SysLog(operation = "取消文件上传", mode = 3)
    @ApiOperation(value = "取消文件上传", notes = "取消文件上传，删除服务端已接收的文件分片和文件信息")
    @ApiImplicitParam(name = "fileId", value = "资源文件ID", required = true, dataType = "String")
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 500, message = "服务器内部错误"),
    })
    @ApiOperationSort(37)
    public ResponseEntity<Void> cancelUpload(@NotBlank(message = "资源ID不可为空") @PathVariable String fileId) {
        String chunkSupPath = catalogService.getCacheSupPath(this.chunkSupPath, fileId);
        chunkStorageService.deleteChunks(chunkSupPath);
        return ResponseEntity.ok().build();
    }

    /*@PostMapping(value = "/resource/update/dir", consumes = {"multipart/form-data"})
    @ApiOperation(value = "上传文件", notes = "上传文件")
    @ApiResponse(code = 200, message = "上传成功")
    @ApiOperationSort(7)
    public ResponseEntity<Void> uploadFile(@Validated WPResourceUploadParam wpResourceUploadParam,
                                           HttpServletRequest request) {
        checkPath(wpResourceUploadParam.getPath(), true);
        AssertUtil.assertThrow("请设置Content-Type: multipart/form-data;", !ServletUtil.isMultipart(request));
        log.debug("上传文件...");
        workspaceResourceService.uploadFile(SecurityUtils.getUser().getUserId(),
            wpResourceUploadParam.getFile(),
            wpResourceUploadParam.getPath());
        return ResponseEntity.ok().build();
    }*/

    @GetMapping("/resource/download")
    @SysLog(operation = "下载文件", mode = 3)
    @ApiOperation(value = "下载文件", notes = "下载文件")
    @ApiOperationSort(8)
    public void downloadFile(WPResourceDownloadParam wpResourceDownloadParam,
                             @ApiParam(value = "用户token(请求头中token则不需要载url上加入)", required = true)
                             @RequestParam("access_token") String token) {
        checkPath(wpResourceDownloadParam.getFilepath(), false);
        String filepath = wpResourceDownloadParam.getFilepath();
        log.debug("开始下载文件: " + filepath);
        workspaceResourceService.downloadFile(SecurityUtils.getUser().getUserId(),
            wpResourceDownloadParam.getFilepath());
        log.debug("文件下载结束...");
    }

    /**
     * 校验路径名称是否合理
     *
     * @param path
     * @param flag 是否允许目录或者文件名为空
     */
    private void checkPath(String path, boolean flag) {
        String pathName = FileUtil.getName(path);
        if (flag && StringUtil.isBlank(pathName)) {
            return;
        }
        AssertUtil.assertThrow("操作目录或者文件失败，请输入正确的目录或者文件名称", FileNameUtil.containsInvalid(pathName));
    }


    private void assertNotOverLimitSize(long length) {
        if (length > storageConfig.getFileMaxSize()) {
            throw new ForbiddenException("文件大小不可超过1G");
        }
    }
}
